/**
 * Licensed under the MIT License
 *
 * Copyright (c) 2010 Alexandre Croiseaux
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */
package marcel.core
{
	import com.asual.swfaddress.SWFAddress;
	import com.asual.swfaddress.SWFAddressEvent;
	import flash.events.EventDispatcher;
	import flash.utils.clearTimeout;
	import flash.utils.getDefinitionByName;
	import flash.utils.setTimeout;
	import marcel.debug.*;
	import marcel.display.UI;
	import marcel.events.FileLoaderEvent;
	import marcel.events.StateEvent;
	import marcel.external.FileType;
	import marcel.utils.ObjectUtils;
	import marcel.utils.StringUtils;
	
	/**
	* @eventType marcel.events.StateEvent.START_LOADING_STATE
	*/
	[Event(name = "onStartLoadingState", type = "marcel.events.StateEvent")]
	/**
	* @eventType marcel.events.StateEvent.END_LOADING_STATE
	*/
	[Event(name = "onEndLoadingState", type = "marcel.events.StateEvent")]
	/**
	* @eventType marcel.events.StateEvent.START_STATE_OPEN
	*/
	[Event(name = "onStartStateOpen", type = "marcel.events.StateEvent")]
	/**
	* @eventType marcel.events.StateEvent.END_STATE_OPEN
	*/
	[Event(name = "onEndStateOpen", type = "marcel.events.StateEvent")]
	/**
	* @eventType marcel.events.StateEvent.START_STATE_CLOSE
	*/
	[Event(name = "onStartStateClose", type = "marcel.events.StateEvent")]
	/**
	* @eventType marcel.events.StateEvent.END_STATE_CLOSE
	*/
	[Event(name = "onEndStateClose", type = "marcel.events.StateEvent")]
	/**
	* @eventType marcel.events.StateEvent.START_UI_OPEN
	*/
	[Event(name = "onStartUIOpen", type = "marcel.events.StateEvent")]
	/**
	* @eventType marcel.events.StateEvent.END_UI_OPEN
	*/
	[Event(name = "onEndUIOpen", type = "marcel.events.StateEvent")]
	/**
	* @eventType marcel.events.StateEvent.START_UI_CLOSE
	*/
	[Event(name = "onStartUIClose", type = "marcel.events.StateEvent")]
	/**
	* @eventType marcel.events.StateEvent.END_UI_CLOSE
	*/
	[Event(name = "onEndUIClose", type = "marcel.events.StateEvent")]
	
	
	/**
	 * StateManager class used to display states of an application by opening and closing UIs
	 * @author Alexandre Croiseaux
	 */
	public class StateManager extends EventDispatcher
	{
		//--------------------------------------------------------------------------
		//
		//  Private vars
		//
		//--------------------------------------------------------------------------
		/**
		 * @private
		 */
		protected var _plugin:Plugin;
		/**
		 * @private
		 */
		protected var _mainLoaded:Boolean;
		/**
		 * @private
		 */
		protected var _loader:UI;
		/**
		 * @private
		 */
		protected var _nbToClose:uint;
		/**
		 * @private
		 */
		protected var _nbToOpen:uint;
		/**
		 * @private
		 */
		protected var _nbClosed:uint;
		/**
		 * @private
		 */
		protected var _nbOpened:uint;
		/**
		 * @private
		 */
		protected var _prevState:String;
		/**
		 * @private
		 */
		protected var _currentState:String;
		/**
		 * @private
		 */
		protected var _nextState:String;
		/**
		 * @private
		 */
		protected var _uis:Object;
		/**
		 * @private
		 */
		protected var _delays:Array;
		/**
		 * @private
		 */
		protected var _params:Object;
		
		
		//--------------------------------------------------------------------------
		//
		//  Public vars
		//
		//--------------------------------------------------------------------------
		/**
		 * Indicates if the view must be locked from user interaction during the transition between 2 states.
		 * @see marcel.core.Plugin.lockView()
		 */
		public var lockViewOnChange:Boolean = false;
		
		//--------------------------------------------------------------------------
		//
		//  Constructor
		//
		//--------------------------------------------------------------------------
		/**
		 * Construct a new StateManager instance
		 * @param	plugin	the Plugin instance to use for handling assetManager, config and flashvars.
		 */
		public function StateManager(plugin:Plugin)
		{
			_plugin = plugin;
			_uis = {};
			_delays = [];
			
			SWFAddress.addEventListener(SWFAddressEvent.EXTERNAL_CHANGE, onSWFAddress);
		}
		
		
		
		//--------------------------------------------------------------------------
		//
		//  Public methods
		//
		//--------------------------------------------------------------------------
		/**
		 * Sets the state defined in the 'state' config var or flashvar
		 */
		public function setDefaultState():void
		{
			setState(_plugin.config.getDefaultState());
		}
		
		/**
		 * Sets the state passed in parameter
		 * @param	id the state id to set
		 * @param	An optional object that will be passed to the setParams() method for all the uis of the specified state
		 */
		public function setState(id:String, params:Object = null):void
		{
			if (!_plugin.config.hasState(id))
			{
				debuge("State '" + id + "' not found !");
				return;
			}
			else if (id == _currentState)
			{
				debugw("State '" + id + "' already opened !");
				return;
			}
			
			// lock view
			if (lockViewOnChange) _plugin.lockView(true);
			
			// clear delays
			for (var i:int = 0; i < _delays.length; i++) clearTimeout(_delays[i]);
			_delays = [];
			
			// save next state
			_nextState = id;
			_params = params;
			
			// main loader
			if (!_mainLoaded)
			{
				loadState(id, _plugin.config.getStatesMainLoader());
			}
			else
			{
				if (_plugin.config.mustLoadStateBeforeClose(_nextState)) loadState(_nextState);
				else closeUIs(_plugin.config.getStateUIsToClose(_nextState));
			}
		}
		
		/**
		 * Go back to previous state if possible.
		 */
		public function prevState():void
		{
			if (_prevState) setState(_prevState);
		}
		
		/**
		 * Returns an UI instance if it's opened
		 * @param	id	The UI id.
		 * @return	en UI instance or null if no opened UI is found for this id.
		 */
		public function getUI(id:String):UI
		{
			return _uis[id];
		}
		
		/**
		 * Returns the previous state id
		 * @return	A string representing the previous state id
		 */
		public function getPrevState():String { return _prevState; }
		/**
		 * Returns the current state id
		 * @return	A string representing the current state id
		 */
		public function getCurrentState():String { return _currentState; }
		/**
		 * Returns the next state id
		 * @return	A string representing the next state id
		 */
		public function getNextState():String { return _nextState; }
		/**
		 * Returns the list of the UIs instances currently opened
		 * @return	A array of UI instances
		 */
		public function getOpenedUIs():/*UI*/Array
		{
			var a:Array = [];
			for (var id:String in _uis)
			{
				if (_uis[id]) a.push(_uis[id]);
			}
			return a;
		}
		
		/**
		 * Returns an object containing key/values specified in the site.cml for the requested state
		 * @param	stateId	The state id to get attributes from
		 * @return	A key/values objct instance
		 */
		public function getStateAttributes(stateId:String):Object
		{
			return ObjectUtils.copyAttributes( { }, _plugin.config.getStateNode(stateId));
		}
		
		
		
		//--------------------------------------------------------------------------
		//
		//  Private methods
		//
		//--------------------------------------------------------------------------
		private function loadState(id:String, loaderUI:String = null):void
		{
			dispatchEvent(new StateEvent(StateEvent.START_LOADING_STATE, _prevState, _currentState, _nextState, null, _params));
			
			if (loaderUI == null) loaderUI = _plugin.config.getStateLoader(id);
			
			// display
			if (_loader)
			{
				_loader.removeEventListener(StateEvent.END_UI_CLOSE, onLoaderClosed);
				if (_loader.parent) _loader.parent.removeChild(_loader);
				_loader.close();
			}
			_loader = buildUI(loaderUI, false);
			if (_loader)
			{
				_loader.addEventListener(StateEvent.END_UI_CLOSE, onLoaderClosed);
				_loader.open();
			}
			
			// assets
			var nodes:XMLList = _plugin.config.getStateAssetsNodes(id, false, !_mainLoaded);
			_plugin.assetManager.addEventListener(FileLoaderEvent.ALL_COMPLETE, onAssetsLoaded);
			_plugin.assetManager.addEventListener(FileLoaderEvent.PROGRESS, onAssetsProgress);
			for each (var node:XML in nodes)
			{
				if (!_plugin.assetManager.hasAsset(node.@id))
				{
					var uri:String = _plugin.config.getAssetUri(node.@id);
					var fileType:FileType = FileType[node.@type.toString().toUpperCase()];
					if (fileType == null) fileType = FileType.getTypeFromExtension(uri);
					_plugin.assetManager.enqueue(node.@id, fileType, uri);
				}
			}
			_plugin.assetManager.startLoad();
		}
		
		private function listenUI(ui:UI, listen:Boolean = true):void
		{
			if (listen)
			{
				ui.addEventListener(StateEvent.END_UI_OPEN, onUIOpened);
				ui.addEventListener(StateEvent.END_UI_CLOSE, onUIClosed);
			}
			else
			{
				ui.removeEventListener(StateEvent.END_UI_OPEN, onUIOpened);
				ui.removeEventListener(StateEvent.END_UI_CLOSE, onUIClosed);
			}
		}
		
		private function buildUI(uiId:String, listen:Boolean = true):UI
		{
			if (getUI(uiId) == null)
			{
				var className:String = _plugin.config.getUIClassName(uiId);
				var clazz:Class;
				try
				{
					clazz = getDefinitionByName(className) as Class;
				}
				catch (e:ReferenceError)
				{
					debuge("Class '" + className + "' not found !");
					return null;
				}
				var ui:UI = new clazz();
				ui.name = uiId;
				if (listen) listenUI(ui);
				_plugin.uisContainer.addChild(ui);
				_uis[uiId] = ui;
				
				// build confData
				ui.confData = { };
				var uiNode:XML = _plugin.config.getUINode(uiId);
				for each (var attr:XML in uiNode.attributes())
				{
					ui.confData[attr.name().toString()] = _plugin.config.parseValue(attr.toString());
				}
				
				if (_params) ui.setParams(_params);
				ui.initialize();
				return ui;
			}
			
			return getUI(uiId);
		}
		
		private function openUIs(uis:/*String*/Array):void
		{
			_prevState = _currentState;
			_currentState = _nextState;
			_nextState = null;
			
			dispatchEvent(new StateEvent(StateEvent.START_STATE_OPEN, _prevState, _currentState, _nextState, null, _params));
			
			// reset vars
			var i:int;
			_nbOpened = 0;
			_nbToOpen = 0;
			
			// push sections to open in a tab (2 boucles à cause des sections qui s'ouvrent en 0 frames et qui déclenchent tout de suite le onUIOpened)
			var uisToOpen:/*UI*/Array = []
			for (i = 0; i < uis.length; i++)
			{
				var ui:UI = buildUI(uis[i]);
				if (ui && !ui.isOpened) uisToOpen.push(ui);
				else if (ui && ui.isOpened && _params) ui.setParams(_params);
			}
			
			updateUIDepths();
			
			// open uis
			_nbToOpen = uisToOpen.length;
			if (_nbToOpen == 0) onUIOpened(null);
			else
			{
				var delays:Object = _plugin.config.getStateDelays(_currentState);
				for (i = 0; i < uisToOpen.length; i++)
				{
					var delay:Number = delays[uisToOpen[i].confData.id];
					if (!isNaN(delay) && delay > 0)
					{
						var delayId:uint = setTimeout(openUI, delay * 1000, uisToOpen[i]);
						_delays.push(delayId);
					}
					else openUI(uisToOpen[i]);
				}
			}
		}
		
		private function closeUIs(uis:/*String*/Array):void
		{
			dispatchEvent(new StateEvent(StateEvent.START_STATE_CLOSE, _prevState, _currentState, _nextState, null, _params));
			
			// reset vars
			var i:int;
			_nbClosed = 0;
			_nbToClose = 0;
			
			// push sections to close in a tab (2 boucles à cause des sections qui s'ouvrent en 0 frames et qui déclenchent tout de suite le onUIClosed)
			var uisToClose:/*UI*/Array = [];
			for (i = 0; i < uis.length; i++)
			{
				var ui:UI = getUI(uis[i]);
				if (ui) uisToClose.push(ui);
			}
			
			// close uis
			_nbToClose = uisToClose.length;
			if (_nbToClose == 0) onUIClosed(null);
			else
			{
				var delays:Object = _plugin.config.getStateDelays(_currentState, true);
				for (i = 0; i < uisToClose.length; i++)
				{
					var delay:Number = delays[uisToClose[i].confData.id];
					if (!isNaN(delay) && delay > 0)
					{
						var delayId:uint = setTimeout(closeUI, delay * 1000, uisToClose[i]);
						_delays.push(delayId);
					}
					else closeUI(uisToClose[i]);
				}
			}
		}
		
		private function openUI(ui:UI):void
		{
			dispatchEvent(new StateEvent(StateEvent.START_UI_OPEN, _prevState, _currentState, _nextState, ui.confData.id, _params));
			ui.open();
		}
		
		private function closeUI(ui:UI):void
		{
			dispatchEvent(new StateEvent(StateEvent.START_UI_CLOSE, _prevState, _currentState, _nextState, ui.confData.id, _params));
			ui.close();
		}
		
		private function updateUIDepths():void
		{
			// sort uis by depths
			var ui:UI;
			var a:/*UI*/Array = [];
			for each (ui in _uis)
			{
				if (ui && ui.parent == _plugin.uisContainer) a.push( { level:int(ui.confData.level), ui:ui } );
			}
			a.sortOn("level", Array.NUMERIC);
			a.reverse();
			
			// set correct depth
			for (var i:int = 0; i < a.length; i++)
			{
				ui = a[i].ui;
				if (ui && ui.parent) ui.parent.setChildIndex(ui, 0);
			}
		}
		
		private function clearLoader():void
		{
			_loader.removeEventListener(StateEvent.END_UI_CLOSE, onLoaderClosed);
			_uis[_loader.confData.id] = null;
			if (_loader.parent) _loader.parent.removeChild(_loader);
			_loader = null;
		}
		
		
		
		
		//--------------------------------------------------------------------------
		//
		//  Events
		//
		//--------------------------------------------------------------------------
		private function onAssetsProgress(e:FileLoaderEvent):void
		{
			if (_loader) _loader.setPercentLoaded(e.percent);
		}
		
		private function onAssetsLoaded(e:FileLoaderEvent):void
		{
			_plugin.assetManager.removeEventListener(FileLoaderEvent.ALL_COMPLETE, onAssetsLoaded);
			_plugin.assetManager.removeEventListener(FileLoaderEvent.PROGRESS, onAssetsProgress);
			
			dispatchEvent(new StateEvent(StateEvent.END_LOADING_STATE, _prevState, _currentState, _nextState, null, _params));
			
			if (_loader) _loader.close();
			else onLoaderClosed(null);
		}
		
		private function onUIOpened(e:StateEvent):void
		{
			if (e)
			{
				_nbOpened++;
				var ui:UI = e.target as UI;
				_plugin.config.sendStat(ui.confData.stat);
				
				// notify
				dispatchEvent(new StateEvent(StateEvent.END_UI_OPEN, _prevState, _currentState, _nextState, ui.confData.id, _params));
			}
			
			if (_nbOpened == _nbToOpen)
			{
				var swfa:String = _plugin.config.getStateSWFAddress(_currentState);
				if (swfa && !StringUtils.beginsWith(SWFAddress.getPath(), swfa)) SWFAddress.setValue(swfa);
				
				var title:String = _plugin.config.getStateTitle(_currentState);
				if (title && SWFAddress.getTitle() != title) SWFAddress.setTitle(title);
				
				_plugin.config.sendStat(_plugin.config.getStateStatId(_currentState));
				
				if (lockViewOnChange) _plugin.lockView(false);
				
				dispatchEvent(new StateEvent(StateEvent.END_STATE_OPEN, _prevState, _currentState, _nextState, null, _params));
				
				_params = null;
			}
		}
		
		private function onUIClosed(e:StateEvent):void
		{
			if (e)
			{
				// clear UI
				var ui:UI = e.target as UI;
				if (ui && ui.parent) ui.parent.removeChild(ui);
				listenUI(ui, false);
				_uis[ui.confData.id] = null;
			
				// remove ui assets
				var nodes:XMLList = _plugin.config.getUIAssetsNodes(ui.name, true);
				var a:Array = [];
				for each (var node:XML in nodes)
				{
					_plugin.assetManager.removeAsset(node.@id);
					a.push(node.@id);
				}
				
				// incr
				_nbClosed++;
				
				// info
				if (a.length > 0) debugi("Removing assets of UI '" + ui.name + "' (" + a.join(",") + ")");
				
				// notify
				dispatchEvent(new StateEvent(StateEvent.END_UI_CLOSE, _prevState, _currentState, _nextState, ui.confData.id, _params));
			}
			
			if (_nbClosed == _nbToClose)
			{
				dispatchEvent(new StateEvent(StateEvent.END_STATE_CLOSE, _prevState, _currentState, _nextState, null, _params));
				
				if (_plugin.config.mustLoadStateBeforeClose(_nextState)) openUIs(_plugin.config.getStateUIsToOpen(_nextState));
				else loadState(_nextState);
			}
		}
		
		private function onLoaderClosed(e:StateEvent):void
		{
			if (_loader) clearLoader();
			
			if (_plugin.config.mustLoadStateBeforeClose(_nextState) && _mainLoaded) closeUIs(_plugin.config.getStateUIsToClose(_nextState));
			else openUIs(_plugin.config.getStateUIsToOpen(_nextState));
			
			if (!_mainLoaded) _mainLoaded = true;
		}
		
		private function onSWFAddress(e:SWFAddressEvent):void
		{
			if (_plugin.config.isBuilt())
			{
				var state:String = _plugin.config.getStateFromSWFAddress(e.path);
				if (_plugin.config.hasState(state)) setState(state);
			}
		}
	}

}